﻿#region Using
using System;
using System.IO;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using JigLibX.Physics;
using JigLibX.Geometry;
using JigLibX.Collision;
using JigLibX.Math;
#endregion Using


namespace LloydGame
{
    public class GameObject : GameComponent
    {
        #region Class Fields
        protected List<GameModel> m_gameModels;
        protected List<GameLight> m_usedLights;

        protected Vector3 m_position;
        protected Vector3 m_rotation;
        protected Vector3 m_scale;

        protected bool m_alive;
        protected bool m_staticFlag;
        protected bool m_selected;
        protected String m_alias;

        protected String m_objectType;

        protected Body m_body;
        protected CollisionSkin m_skin;

        public enum drawMode { AMBIENT, LIGHTS, MASK };
        #endregion Class Fields

        #region Properties
        public bool alive
        {
            get
            {
                return m_alive;
            }
            set
            {
                m_alive = value;
            }
        }

        public bool staticFlag
        {
            get
            {
                return m_staticFlag;
            }
        }

        public bool selected
        {
            get
            {
                return m_selected;
            }
            set
            {
                m_selected = value;
            }
        }

        public String objectType
        {
            get
            {
                return m_objectType;
            }
        }

        public CollisionSkin skin
        {
            get
            {
                return m_skin;
            }
        }

        public Body body
        {
            get
            {
                return m_body;
            }
            set
            {
                m_body = value;
            }
        }

        public Vector3 position
        {
            get
            {
                return m_position;
            }
            set
            {
                m_position = value;
            }
        }

        public Vector3 rotation
        {
            get
            {
                return m_rotation;
            }
            set
            {
                m_rotation = value;
            }
        }

        public String alias
        {
            get
            {
                return m_alias;
            }
            set
            {
                m_alias = value;
            }
        }

        public Vector3 scale
        {
            get
            {
                return m_scale;
            }
            set
            {
                m_scale = value;
            }
        }

        public List<GameModel> gameModels
        {
            get
            {
                return m_gameModels;
            }
        }

        public List<GameLight> usedLights
        {
            get
            {
                return m_usedLights;
            }
            set
            {
                m_usedLights = value;
            }
        }
        #endregion Properties

        #region Constructors and Initialization
        // constructor
        public GameObject( Game p_game, GameWorld p_gameWorld, String p_objectType, Vector3 p_position
                         , Vector3 p_rotation, Vector3 p_scale, bool p_staticFlag, String p_alias) 
            : base(p_game)
        {
            m_gameModels = new List<GameModel>();
            m_objectType = p_objectType;
            
            m_rotation = p_rotation;
            m_position = p_position;
            m_scale = p_scale;

            m_alive = true;
            m_staticFlag = p_staticFlag;
            m_selected = false;
            m_alias = p_alias;

            m_body = new Body();
            m_skin = new CollisionSkin(m_body);
            m_body.CollisionSkin = m_skin;
           
            m_body.Immovable = p_staticFlag;

            loadObjectType("GameContent\\Objects\\" + p_objectType + ".txt", p_gameWorld);

            m_body.CollisionSkin.callbackFn += new CollisionCallbackFn(collision_callbackFn);

            p_game.Components.Add(this);
        }

        protected virtual bool collision_callbackFn(CollisionSkin skin0, CollisionSkin skin1)
        {
            GameObject obj0 = ((Game1)Game).gameWorld.GetObject(skin0.Owner);
            GameObject obj1 = ((Game1)Game).gameWorld.GetObject(skin1.Owner);

            if (obj0 == null || obj1 == null)
            {
                //Console.WriteLine("This should not happen! :S");
                return false;
            }
            
            if (obj0 is CarObject && (obj1 is CoinObject || obj1 is PowerUpObject || obj1 is JumpObject))
            {
                return false;
            }
            else if (obj1 is CarObject && (obj0 is CoinObject || obj0 is PowerUpObject || obj0 is JumpObject))
            {
                return false;
            }

            return true;
        }

        // load object type specific information
        protected void loadObjectType(String p_ObjectTypeFile, GameWorld p_gameWorld)
        {
            char[] charSeparators = new char[] { ' ', '\t' };
            
            try
            {
                using (StreamReader sr = new StreamReader(p_ObjectTypeFile))
                {
                    String line;

                    // read the file line by line
                    while ((line = sr.ReadLine()) != null)
                    {
                        if (line.StartsWith("#")) continue; // Skip comments

                        string[] tokens = line.Split(charSeparators, System.StringSplitOptions.RemoveEmptyEntries);

                        if (tokens.Length == 0) continue; // Skip empty rows

                        if (line.StartsWith("model:"))
                        {
                            addModel(tokens);
                        }
                        else if (line.StartsWith("phys:"))
                        {
                            addCollisionSkin(tokens);
                        }
                        else if (line.StartsWith("mass:"))
                        {
                            addMass(tokens);
                        }
                        else if (line.StartsWith("pointLight:") || line.StartsWith("spotLight:"))
                        {
                            addLight(tokens, p_gameWorld);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("The file could not be read, or wasn't properly formated:");
                Console.WriteLine(e.Message);
            }
        }

        // Load model files
        private void addModel(String[] p_tokens)
        {
            Vector3 gmPosition = new Vector3(
                float.Parse(p_tokens[2], CultureInfo.InvariantCulture.NumberFormat),
                float.Parse(p_tokens[3], CultureInfo.InvariantCulture.NumberFormat),
                float.Parse(p_tokens[4], CultureInfo.InvariantCulture.NumberFormat));

            Vector3 gmRotation = new Vector3(
                MathHelper.ToRadians(float.Parse(p_tokens[5], CultureInfo.InvariantCulture.NumberFormat)),
                MathHelper.ToRadians(float.Parse(p_tokens[6], CultureInfo.InvariantCulture.NumberFormat)),
                MathHelper.ToRadians(float.Parse(p_tokens[7], CultureInfo.InvariantCulture.NumberFormat)));

            Vector3 gmScale = new Vector3(
                float.Parse(p_tokens[8], CultureInfo.InvariantCulture.NumberFormat),
                float.Parse(p_tokens[9], CultureInfo.InvariantCulture.NumberFormat),
                float.Parse(p_tokens[10], CultureInfo.InvariantCulture.NumberFormat));

            GameModel gameModel = new GameModel(Game, p_tokens[1], gmPosition, gmRotation, gmScale);

            gameModels.Add(gameModel);
        }

        // adds a primitive to the collision skin
        private void addCollisionSkin(String[] p_tokens)
        {

            // these vectors will be treated diffrently depending on skin primitive
            Vector3 spPosition = new Vector3(
                float.Parse(p_tokens[2], CultureInfo.InvariantCulture.NumberFormat),
                float.Parse(p_tokens[3], CultureInfo.InvariantCulture.NumberFormat),
                float.Parse(p_tokens[4], CultureInfo.InvariantCulture.NumberFormat));

            Vector3 spRotation = new Vector3(
                MathHelper.ToRadians(float.Parse(p_tokens[5], CultureInfo.InvariantCulture.NumberFormat)),
                MathHelper.ToRadians(float.Parse(p_tokens[6], CultureInfo.InvariantCulture.NumberFormat)),
                MathHelper.ToRadians(float.Parse(p_tokens[7], CultureInfo.InvariantCulture.NumberFormat)));

            Vector3 spSideLength = new Vector3(
                float.Parse(p_tokens[8], CultureInfo.InvariantCulture.NumberFormat),
                float.Parse(p_tokens[9], CultureInfo.InvariantCulture.NumberFormat),
                float.Parse(p_tokens[10], CultureInfo.InvariantCulture.NumberFormat));

            Primitive skinPrimitive = null;

            // descide which primitive is used
            switch (p_tokens[1])
            {
                case "box":
                    skinPrimitive = new Box(spSideLength * m_scale * (spPosition + new Vector3(-0.5f))
                        , Matrix.CreateFromYawPitchRoll(spRotation.Y, spRotation.X, spRotation.Z)
                        , spSideLength * m_scale);
                    break;
                case "sphere":
                    skinPrimitive = new Sphere(spPosition, spRotation.X * m_scale.X);
                    break;
                case "capsule":
                    skinPrimitive = new Capsule(spPosition
                        , Matrix.CreateFromYawPitchRoll(spRotation.Y, spRotation.X, spRotation.Z)
                        , spSideLength.X * m_scale.X, spSideLength.Y * m_scale.X);
                    break;
                case "plane":
                    skinPrimitive = new JigLibX.Geometry.Plane(spPosition, spRotation);
                    break;
            }

            int materialID = (int)MaterialTable.MaterialID.UserDefined;
            Object materialProps = null;

            if (p_tokens.Length == 12) // uses materialID
            {
                materialID = int.Parse(p_tokens[11]);
            }
            else // uses custom defined material
            {
                materialProps = new MaterialProperties(
                    float.Parse(p_tokens[11], CultureInfo.InvariantCulture.NumberFormat),
                    float.Parse(p_tokens[12], CultureInfo.InvariantCulture.NumberFormat),
                    float.Parse(p_tokens[13], CultureInfo.InvariantCulture.NumberFormat));
            }

            m_skin.AddPrimitive(skinPrimitive, materialID, (MaterialProperties)materialProps);
        }

        // set the objects mass
        private void addMass(String[] p_tokens)
        {
            float mass = float.Parse(p_tokens[1], CultureInfo.InvariantCulture.NumberFormat);

            PrimitiveProperties primitiveProperties = new PrimitiveProperties(
                PrimitiveProperties.MassDistributionEnum.Solid,
                PrimitiveProperties.MassTypeEnum.Mass, mass);

            float junk;
            Vector3 com;
            Matrix it;
            Matrix itCoM;

            m_skin.GetMassProperties(primitiveProperties, out junk, out com, out it, out itCoM);

            m_body.BodyInertia = itCoM;
            m_body.Mass = junk;

            m_body.MoveTo(m_position, Matrix.CreateFromYawPitchRoll(m_rotation.Y, m_rotation.X, m_rotation.Z));
            m_skin.ApplyLocalTransform(new Transform(-com, Matrix.Identity));
            m_body.EnableBody();
        }

        // attach a light source to the object
        private void addLight(String[] p_tokens, GameWorld p_gameWorld)
        {
            GameLight light = null;

            Vector3 lightOffset = new Vector3(
                float.Parse(p_tokens[1], CultureInfo.InvariantCulture.NumberFormat),
                float.Parse(p_tokens[2], CultureInfo.InvariantCulture.NumberFormat),
                float.Parse(p_tokens[3], CultureInfo.InvariantCulture.NumberFormat));

            float lightFalloffStart = float.Parse(p_tokens[4], CultureInfo.InvariantCulture.NumberFormat);
            float lightFalloffEnd = float.Parse(p_tokens[5], CultureInfo.InvariantCulture.NumberFormat);

            Vector3 lighColor = new Vector3(
                float.Parse(p_tokens[6], CultureInfo.InvariantCulture.NumberFormat),
                float.Parse(p_tokens[7], CultureInfo.InvariantCulture.NumberFormat),
                float.Parse(p_tokens[8], CultureInfo.InvariantCulture.NumberFormat));

            if (p_tokens[0] == "PointLight")
            {
                light = new GamePointLight(Game, Vector3.Zero, lightFalloffStart, lightFalloffEnd, lighColor, 256);
            }
            else
            {
                Vector3 lightDirection = new Vector3(
                    float.Parse(p_tokens[9], CultureInfo.InvariantCulture.NumberFormat),
                    float.Parse(p_tokens[10], CultureInfo.InvariantCulture.NumberFormat),
                    float.Parse(p_tokens[11], CultureInfo.InvariantCulture.NumberFormat));

                float lightCutoffStart = float.Parse(p_tokens[12], CultureInfo.InvariantCulture.NumberFormat);
                float lightCutoffEnd = float.Parse(p_tokens[13], CultureInfo.InvariantCulture.NumberFormat);
                float lightCutoffExp = float.Parse(p_tokens[14], CultureInfo.InvariantCulture.NumberFormat);

                light = new GameSpotLight(Game, Vector3.Zero, lightFalloffStart, lightFalloffEnd, lighColor
                                , lightDirection, lightCutoffStart, lightCutoffEnd, lightCutoffExp, 512);
            }

            light.attachTo(this, lightOffset * m_scale);

            p_gameWorld.gameLights.Add(light);
        }
        #endregion Constructors and Initialization

        #region Draw
        // draw the object
        public virtual void draw(GameTime p_gameTime, drawMode p_drawMode)
        {
            BoundingFrustum viewFrustum = new BoundingFrustum(((Game1)Game).camera.viewProj);

            foreach(GameModel gameModel in m_gameModels)
            {
                BoundingSphere modelBound = new BoundingSphere(m_position + gameModel.position, 1);

                // calculate the entire models bounding sphere
                foreach (ModelMesh mesh in gameModel.model.Meshes)
                {
                    BoundingSphere meshBound = mesh.BoundingSphere;

                    meshBound.Center = m_position + gameModel.position;
                    
                    modelBound = BoundingSphere.CreateMerged(modelBound, meshBound);
                }

                modelBound.Radius *= MathHelper.Max(MathHelper.Max(m_scale.X, m_scale.Y), m_scale.Z) *
                    MathHelper.Max(MathHelper.Max(gameModel.scale.X, gameModel.scale.Y), gameModel.scale.Z);

                // cull away if the entire model is outside the view frustum
                if (viewFrustum.Contains(modelBound) == ContainmentType.Disjoint)
                {
                    continue;
                }

                Matrix world =
                    Matrix.CreateScale(m_scale * gameModel.scale) *
                    Matrix.CreateFromYawPitchRoll(
                        m_rotation.Y + gameModel.rotation.Y,
                        m_rotation.X + gameModel.rotation.X,
                        m_rotation.Z + gameModel.rotation.Z) *
                    Matrix.CreateTranslation(m_position + gameModel.position);

                Matrix wvp = world * ((Game1)Game).camera.viewProj;

                foreach (ModelMesh mesh in gameModel.model.Meshes)
                {
                    // calculate the mesh's bounding sphere
                    BoundingSphere meshBound = mesh.BoundingSphere;

                    meshBound.Center = m_position + gameModel.position;
                    meshBound.Radius *=
                        MathHelper.Max(MathHelper.Max(m_scale.X, m_scale.Y), m_scale.Z) *
                        MathHelper.Max(MathHelper.Max(gameModel.scale.X, gameModel.scale.Y), gameModel.scale.Z);

                    // cull away if the mesh is outside the view frustum
                    if (viewFrustum.Contains(meshBound) == ContainmentType.Disjoint)
                    {
                        continue;
                    }

                    foreach (ModelMeshPart meshPart in mesh.MeshParts)
                    {
                        if (p_drawMode == drawMode.AMBIENT)
                        {
                            drawWithAmbient(p_gameTime, mesh, meshPart, world, wvp);
                        }
                        else if (p_drawMode == drawMode.LIGHTS)
                        {
                            drawWithLights(p_gameTime, mesh, meshPart, world, wvp);
                        }
                        else if (p_drawMode == drawMode.MASK)
                        {
                            drawWithMask(p_gameTime, mesh, meshPart, world, wvp);
                        }
                    }
                }
            }
        }

        public void drawWithLights(GameTime p_gameTime, ModelMesh p_mesh, ModelMeshPart p_meshPart
            , Matrix p_world, Matrix p_wvp)
        {
            Effect effect = p_meshPart.Effect;

            effect.CurrentTechnique = effect.Techniques["BasicLighting"];

            effect.Parameters["world"].SetValue(p_world);
            effect.Parameters["wvp"].SetValue(p_wvp);

            effect.Parameters["cameraPosition"].SetValue(((Game1)Game).camera.position);  // shared
            effect.Parameters["specularPower"].SetValue(100.0f);

            effect.Parameters["materialColor"].SetValue(Color.White.ToVector4());
            effect.Parameters["fresnelBase"].SetValue(new Vector3(0.17f, 0.17f, 0.17f));

            // draw the object as many times it takes to get the contribution from all lights
            foreach (GameLight gameLight in m_usedLights)
            {
                EffectParameter lightParameter = effect.Parameters["light"];
                gameLight.UpdateLight(lightParameter, p_world);

                String currentPass = null;

                Texture shadowMap = gameLight.shadowMap;

                if (shadowMap == null)
                {
                    effect.Parameters["castShadow"].SetValue(false);
                }
                else
                {
                    effect.Parameters["castShadow"].SetValue(true);

                }

                if (gameLight is GameSpotLight)
                {
                    effect.Parameters["shadowMap2D"].SetValue(shadowMap);

                    currentPass = "SpotLight";
                }
                else
                {
                    effect.Parameters["shadowMapCube"].SetValue(shadowMap);

                    currentPass = "PointLight";
                }

                // draw the object
                effect.Begin();
                effect.CurrentTechnique.Passes[currentPass].Begin();

                drawMeshPart(p_mesh, p_meshPart);

                effect.CurrentTechnique.Passes[currentPass].End();
                effect.End();
            }
        }

        public void drawWithAmbient(GameTime p_gameTime, ModelMesh p_mesh, ModelMeshPart p_meshPart
            , Matrix p_world, Matrix p_wvp)
        {
            Effect effect = p_meshPart.Effect;

            effect.CurrentTechnique = effect.Techniques["BasicLighting"];

            effect.Parameters["world"].SetValue(p_world);
            effect.Parameters["wvp"].SetValue(p_wvp);
            effect.Parameters["wv"].SetValue(p_world * ((Game1)Game).camera.view);

            effect.Parameters["cameraFarClip"].SetValue(((Game1)Game).camera.farClip);  // shared

            if (m_selected)
            {
                effect.Parameters["materialColor"].SetValue(Color.Red.ToVector4());
                effect.Parameters["ambientLightColor"].SetValue(new Vector4(3.0f));  // shared
            }
            else
            {
                effect.Parameters["materialColor"].SetValue(Color.White.ToVector4());
                effect.Parameters["ambientLightColor"].SetValue(new Vector4(1.0f));  // shared
            }

            effect.Begin();
            effect.CurrentTechnique.Passes["Ambient"].Begin();

            drawMeshPart(p_mesh, p_meshPart);

            effect.CurrentTechnique.Passes["Ambient"].End();
            effect.End();
        }

        public void drawWithMask(GameTime p_gameTime, ModelMesh p_mesh, ModelMeshPart p_meshPart
            , Matrix p_world, Matrix p_wvp)
        {
            Effect effect = p_meshPart.Effect;

            effect.CurrentTechnique = effect.Techniques["BasicLighting"];

            effect.Parameters["world"].SetValue(p_world);
            effect.Parameters["wvp"].SetValue(p_wvp);

            effect.Begin();
            effect.CurrentTechnique.Passes["Mask"].Begin();

            drawMeshPart(p_mesh, p_meshPart);

            effect.CurrentTechnique.Passes["Mask"].End();
            effect.End();
        }

        public void drawMeshPart(ModelMesh p_mesh, ModelMeshPart p_meshPart)
        {
            // set the vertex source to the mesh's vertex buffer
            Game.GraphicsDevice.Vertices[0].SetSource(
                p_mesh.VertexBuffer, p_meshPart.StreamOffset,
                p_meshPart.VertexStride);

            // set the vertex declaration
            Game.GraphicsDevice.VertexDeclaration = p_meshPart.VertexDeclaration;

            // set the current index buffer to the mesh's index buffer
            Game.GraphicsDevice.Indices = p_mesh.IndexBuffer;

            Game.GraphicsDevice.DrawIndexedPrimitives(
                Microsoft.Xna.Framework.Graphics.PrimitiveType.TriangleList, p_meshPart.BaseVertex, 0,
                p_meshPart.NumVertices, p_meshPart.StartIndex,
                p_meshPart.PrimitiveCount);
        }
        #endregion Draw
        
        #region Update
        // update the object
        public override void Update(GameTime p_gameTime)
        {
            
            m_position = m_body.Position;
                       
            Matrix o = m_skin.GetPrimitiveLocal(0).Transform.Orientation;            
           
            m_rotation.X = MathHelper.WrapAngle((float)-Math.Asin(MathHelper.Clamp(o.M32, -1.0f, 1.0f)));
            m_rotation.Y = MathHelper.WrapAngle((float)Math.Atan2(o.M31, o.M33));
            m_rotation.Z = MathHelper.WrapAngle((float)Math.Atan2(o.M12, o.M22));

            o = m_body.Orientation;
                       
            m_rotation.X += MathHelper.WrapAngle((float)-Math.Asin(MathHelper.Clamp(o.M32, -1.0f, 1.0f)));
            m_rotation.Y += MathHelper.WrapAngle((float)Math.Atan2(o.M31, o.M33));
            m_rotation.Z += MathHelper.WrapAngle((float)Math.Atan2(o.M12, o.M22));
            
        }
        #endregion Update
    }
}
